/*
 * Copyright (c) 2007 Argonne National Laboratory. All rights reserved.
 */
package ratkit.repast.schedule;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import repast.simphony.engine.schedule.DefaultActionFactory;
import repast.simphony.engine.schedule.IAction;
import repast.simphony.engine.schedule.IActionParameterPair;
import repast.simphony.engine.schedule.ScheduleParameters;
import repast.simphony.engine.schedule.ScheduledMethod;
import repast.simphony.util.ClassUtilities;

/**
 * Default action factory for producing {@code RatKitCallBackAction} to be
 * executed by the {@code RatKitSchedule}.
 * 
 * @author İbrahim Çakırlar
 * @version $Revision: 1.0
 * @see repast.simphony.engine.schedule.DefaultActionFactory
 * @see repast.simphony.engine.schedule.Schedule
 * @see repast.simphony.engine.schedule.IAction
 */
@SuppressWarnings("rawtypes")
public class RatKitActionFactory extends DefaultActionFactory {

	final long serialVersionUID = 1146802409368283440L;

	/**
	 * Creates an IAction that will call the named method on the specified
	 * target using the specified parameters.
	 * 
	 * @param target
	 *            the object on which to call the name method
	 * @param methodName
	 *            the name of the method to call
	 * @param parameters
	 *            the parameters of the method
	 * @return an IAction that will call the named method on the specified
	 *         target using the specified parameters.
	 */
	public IAction createAction(Object target, String methodName,
			Object... parameters) {
		return new RatKitCallBackAction(target, methodName, parameters);
	}

	/**
	 * Creates an IActionParameterPair from annotated methods in the specified
	 * annotatedObj. The IAction will execute the method whose parameters best
	 * match the specified parameters. The parameters will be passed to the
	 * method at execution.
	 * 
	 * @param annotatedObj
	 *            the object containing the annotated methods
	 * @param parameters
	 *            the parameters to pass to the method call and to use to find
	 *            the method itself
	 * @return an IActionParameterPair containing the best matching IAction and
	 *         its ScheduleParameters.
	 * 
	 * @see repast.simphony.engine.schedule.ScheduledMethod
	 * @see repast.simphony.engine.schedule.IActionParameterPair
	 */
	public IActionParameterPair createAction(Object annotatedObj,
			Object... parameters) {
		Class clazz = annotatedObj.getClass();
		Map<String, ScheduleParameters> nameParamMap = findCandidateMethodMatches(clazz);

		Class[] paramTypes = new Class[parameters.length];
		for (int i = 0; i < parameters.length; i++) {
			paramTypes[i] = parameters[i].getClass();
		}

		List<Method> matches = new ArrayList<Method>();
		for (String name : nameParamMap.keySet()) {
			Method method = ClassUtilities.findMethod(clazz, name, paramTypes);
			if (method != null) {
				matches.add(method);
			}
		}

		if (matches.size() != 1) {
			String pTypes = "(";
			for (int i = 0; i < paramTypes.length; i++) {
				if (i > 0)
					pTypes += ", ";
				pTypes += paramTypes[i].getName();
			}
			pTypes += ")";
			if (matches.size() == 0)
				throw new IllegalArgumentException(
						"Unable to find match for ScheduleMethod annotated object '"
								+ clazz.getName() + "' with method parameters "
								+ pTypes);
			else
				throw new IllegalArgumentException(
						"Too many matches for ScheduleMethod annotated object '"
								+ clazz.getName() + "' with method parameters "
								+ pTypes);
		}

		Method method = matches.get(0);
		RatKitCallBackAction action = new RatKitCallBackAction(annotatedObj,
				method, parameters);
		return new IActionParameterPair(action, nameParamMap.get(method
				.getName()));
	}

	private Map<String, ScheduleParameters> findCandidateMethodMatches(
			Class clazz) {
		Method[] methods = clazz.getMethods();
		// will contain the list of candidate method names
		Map<String, ScheduleParameters> nameParamMap = new HashMap<String, ScheduleParameters>();
		for (Method method : methods) {
			ScheduledMethod scheduledMethod = deepAnnotationCheck(method,
					ScheduledMethod.class);

			if (scheduledMethod != null) {
				String methodName = method.getName();
				ScheduleParameters params;
				double duration = scheduledMethod.duration();
				if (scheduledMethod.interval() > 0) {
					params = ScheduleParameters.createRepeating(
							scheduledMethod.start(),
							scheduledMethod.interval(),
							scheduledMethod.priority(), duration);
				} else {
					params = ScheduleParameters.createOneTime(
							scheduledMethod.start(),
							scheduledMethod.priority(), duration);
				}
				nameParamMap.put(methodName, params);
			}
		}
		return nameParamMap;
	}

	public <T extends Annotation> T deepAnnotationCheck(Method method,
			Class<T> annotationClass) throws SecurityException {
		T annotationInstance = method.getAnnotation(annotationClass);

		if (annotationInstance != null) {
			return annotationInstance;
		}

		String methodName = method.getName();
		Class<?>[] parameterTypes = method.getParameterTypes();

		Class<?> currentClass = method.getDeclaringClass();

		while (currentClass != null) {

			Method superClassMethod = findMethod(currentClass, methodName,
					parameterTypes);

			if (superClassMethod != null) {
				T superClassAnnotation = superClassMethod
						.getAnnotation(annotationClass);

				if (superClassAnnotation != null) {
					return superClassAnnotation;
				}
			}

			Class[] declaringClassInterfaces = currentClass.getInterfaces();
			for (Class inter : declaringClassInterfaces) {
				Method interfaceMethod = findMethod(inter, methodName,
						parameterTypes);

				if (interfaceMethod != null) {
					T interfaceAnnotation = interfaceMethod
							.getAnnotation(annotationClass);

					if (interfaceAnnotation != null) {
						return interfaceAnnotation;
					}
				}
			}

			currentClass = currentClass.getSuperclass();
		}

		return null;
	}

	public Method findMethod(Class<?> clazz, String methodName,
			Class<?>... params) {
		Class<?> origClass = clazz;
		while (clazz != null) {
			Method[] methods = clazz.getMethods();
			Method method = doFindMethod(methods, methodName, params);
			if (method != null) {
				return method;
			}
			clazz = clazz.getSuperclass();
		}

		// if we get here then we know that no method has been found
		// so see if we any of the params are Object versions of the primitives
		// and try to find the primitives
		Class<?> newParams[] = new Class<?>[params.length];
		boolean search = false;
		for (int i = 0; i < params.length; i++) {
			Class<?> param = params[i];
			Class<?> primitive = convertObjectPrimitive(param);
			if (primitive != null) {
				newParams[i] = primitive;
				search = true;
			} else {
				newParams[i] = param;
			}
		}

		if (search) {
			clazz = origClass;
			while (clazz != null) {
				Method[] methods = clazz.getMethods();
				Method method = doFindMethod(methods, methodName, newParams);
				if (method != null) {
					return method;
				}
				clazz = clazz.getSuperclass();
			}
		}

		return null;
	}

	private Class<?> convertObjectPrimitive(Class clazz) {
		if (clazz.equals(Double.class)) {
			return double.class;
		}
		if (clazz.equals(Long.class)) {
			return long.class;
		}
		if (clazz.equals(Integer.class)) {
			return int.class;
		}
		if (clazz.equals(Float.class)) {
			return float.class;
		}
		if (clazz.equals(Boolean.class)) {
			return boolean.class;
		}
		if (clazz.equals(Byte.class)) {
			return byte.class;
		}
		if (clazz.equals(Short.class)) {
			return short.class;
		}

		return null;
	}

	private Method doFindMethod(Method[] methods, String methodName,
			Class<?>... params) {
		for (Method method : methods) {
			if (methodName.equals(method.getName())) {
				// found a matching name, check args
				Class<?>[] methodParams = method.getParameterTypes();

				// allow for params == null to mean the method takes 0 args
				if (methodParams.length == 0 && params == null) {
					return method;
				} else if (methodParams.length != 0 && params == null) {
					// skip this case
				} else if (methodParams.length == params.length) {
					boolean match = true;
					for (int i = 0; i < params.length; i++) {
						if (!methodParams[i].isAssignableFrom(params[i])) {
							match = false;
							break;
						}
					}
					if (match) {
						return method;
					}
				}
			}
		}

		return null;
	}

	/**
	 * Creates a List of IActionParameterPair-s from annotated methods. The list
	 * is created from the methods in the specified object that are annotated by
	 * the ScheduledMethod annotation.
	 * 
	 * @param obj
	 *            the object whose methods are annotated
	 * @return a List of IAction together with their scheduling parameter data
	 *         created from annotated methods.
	 * @see repast.simphony.engine.schedule.ScheduledMethod
	 * @see repast.simphony.engine.schedule.IActionParameterPair
	 */
	public List<IActionParameterPair> createActions(Object obj) {
		Class clazz = obj.getClass();
		Method[] methods = clazz.getMethods();
		List<IActionParameterPair> actions = new ArrayList<IActionParameterPair>();
		for (Method method : methods) {
			ScheduledMethod scheduledMethod = method
					.getAnnotation(ScheduledMethod.class);
			if (scheduledMethod != null
					&& method.getParameterTypes().length == 0
					&& scheduledMethod.start() != ScheduledMethod.NO_PARAMETERS) {
				RatKitCallBackAction action = new RatKitCallBackAction(obj,
						method);
				ScheduleParameters params;
				double duration = scheduledMethod.duration();
				if (scheduledMethod.start() == ScheduledMethod.END) {
					params = ScheduleParameters.createAtEnd(scheduledMethod
							.priority());
				} else if (scheduledMethod.interval() > 0) {
					params = ScheduleParameters.createRepeating(
							scheduledMethod.start(),
							scheduledMethod.interval(),
							scheduledMethod.priority(), duration);
				} else {
					params = ScheduleParameters.createOneTime(
							scheduledMethod.start(),
							scheduledMethod.priority(), duration);
				}
				actions.add(new IActionParameterPair(action, params));
			}
		}
		return actions;
	}
}
